﻿namespace Microsoft.Services.TestTools.UITesting.Html
{
    using System;
    using System.Drawing;
    using Microsoft.VisualStudio.TestTools.UITesting;
    using Microsoft.VisualStudio.TestTools.UITesting.HtmlControls;

    /// <summary>
    ///   <see cref="UITestControl"/> extension methods
    /// </summary>
    public static class UITestControlExtensions
    {

        #region click

        /// <summary>
        /// Clicks on the control.
        /// </summary>
        /// <param name="self">The control to click on</param>
        /// <remarks>The method waits for control to exist and that it's clickable</remarks>
        public static void Click(this UITestControl self)
        {
            Click(self, new Point(1, 1));
        }

        /// <summary>
        /// Clicks on the control.
        /// </summary>
        /// <param name="self">The control to click on</param>
        /// <param name="relativeCoordinate">The relative coordinate.</param>
        public static void Click(this UITestControl self, Point relativeCoordinate)
        {
            self.WaitForControlExist();
            //self.EnsureClickable();
            Mouse.Click(self, relativeCoordinate);
        }

        /// <summary>
        /// Clicks on the matching control
        /// </summary>
        /// <param name="container">The container control.</param>
        /// <param name="findCriterias">The find criterias.</param>
        /// <example>
        /// Initialize an anonymous type with your find criterias. 
        /// The criterias should be any of the property names for the specific control.
        /// <seealso cref="HtmlControl.PropertyNames"/>
        /// <code>
        /// //launch the browser
        /// BrowserWindow browser = BrowserWindow.Launch(new Uri("..."));
        /// //find a control with id "ButtonId" and then click on it
        /// browser.Click(new { Id = "ButtonId" });
        /// </code>
        /// </example>
        public static void Click(this UITestControl container, object findCriterias)
        {
            container.Find(findCriterias).Click();
        }

        /// <summary>
        /// Clicks on a <see cref="HtmlHyperlink"/> with the specified inner text.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="text">The text.</param>
        public static void ClickOnLink(this UITestControl container, string text)
        {
            container
                .Find<HtmlHyperlink>(new { InnerText = text })
                .Click();
        }

        /// <summary>
        /// Clicks on a button with the specified text.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="text">The text.</param>
        /// <remarks>
        /// This method will fist search for a <see cref="HtmlButton"/> (HTML tag "button") with the specified text and click on it.
        /// If no matching HtmlButton is found the method will instead search for a <see cref="HtmlInputButton"/> 
        /// (HTML tag "input" with type attribute equal to "button").
        /// </remarks>
        public static void ClickOnButton(this UITestControl container, string text)
        {
            if (!IfExists<HtmlButton>(container, new { InnerText = text }, e => e.Click()))
            {
                Find<HtmlInputButton>(container, new { Value = text }).Click();
            }
        }

        #endregion

        #region check

        /// <summary>
        /// Checks a <see cref="HtmlCheckBox"/> with the specified Id.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="id">The id.</param>
        public static void Check(this UITestControl container, string id)
        {
            container.Check(new { Id = id });
        }

        /// <summary>
        /// Checks a <see cref="HtmlCheckBox"/> matching the specified criteras.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="findCriterias">The find criteras</param>
        public static void Check(this UITestControl container, object findCriterias)
        {
            container.Find<HtmlCheckBox>(findCriterias).Checked = true;
        }

        #endregion

        #region set text

        /// <summary>
        /// Sets the text of the matching <see cref="HtmlEdit"/>.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="findCriteria">The find criteria.</param>
        /// <param name="text">The text.</param>
        public static void SetEditText(this UITestControl container, object findCriteria, string text)
        {
            container
                .Find<HtmlEdit>(findCriteria)
                .Text = text;
        }

        /// <summary>
        /// Sets the text of the <see cref="HtmlEdit"/> with the specified Id.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="id">The id.</param>
        /// <param name="text">The text.</param>
        public static void SetEditText(this UITestControl container, string id, string text)
        {
            container
                .Find<HtmlEdit>(id)
                .Text = text;
        }

        /// <summary>
        /// Sets the text of multiple <see cref="HtmlEdit"/>'s.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="fieldModel">The field model.</param>
        /// <remarks>
        /// This method can be usefull when filling out big forms.
        /// The <paramref name="fieldModel"/> is an anonymous type (object) with properties matching the Id's 
        /// of the <see cref="HtmlEdit"/>'s you want to set the text on.
        /// </remarks>
        /// <example>
        /// Sets all the <see cref="HtmlEdit"/> texts specified.
        /// The property name is the HtmlEdit control id
        /// <code>
        /// <![CDATA[
        /// browser.SetEditTexts(new {
        ///     FirstName = "First",
        ///     LastName  = "Last"
        /// });
        /// ]]>
        /// </code>
        /// </example>
        public static void SetEditTexts(this UITestControl container, object fieldModel)
        {
            fieldModel.ForEachProperty((prop, val) =>
                container.SetEditText(prop, val));
        }

        #endregion

        #region select in list

        /// <summary>
        /// Selects the item with the specified index in the <see cref="HtmlComboBox"/> with the specified id.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="id">The id.</param>
        /// <param name="itemIndex">Index of the item.</param>
        public static void Select(this UITestControl container, string id, int itemIndex)
        {
            container
                .Find<HtmlComboBox>(id)
                .SelectedIndex = itemIndex;
        }

        /// <summary>
        /// Selects the item with the specified text in the <see cref="HtmlComboBox"/> with the specified id.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="id">The id.</param>
        /// <param name="item">Value of the item.</param>
        public static void Select(this UITestControl container, string id, string item)
        {
            container
                .Find<HtmlComboBox>(id)
                .SelectedItem = item;
        }

        #endregion

        #region find

        /// <summary>
        /// Finds the <see cref="HtmlControl"/> with the specified Id.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="id">The id.</param>
        /// <returns>The matching <see cref="HtmlControl"/></returns>
        public static UITestControl Find(this UITestControl container, string id)
        {
            return container.Find<UITestControl>(id);
        }

        /// <summary>
        /// Finds the <see cref="HtmlControl"/> matching the specified criterias.
        /// </summary>
        /// <param name="container">The container.</param>
        /// <param name="findCriteria">The criterias.</param>
        /// <returns>The matching <see cref="HtmlControl"/></returns>
        /// <example>
        /// Find all A elements
        /// <code>
        /// <![CDATA[
        /// HtmlControl control = browser.Find(new { TagName = "A" });
        /// ]]>
        /// </code>
        /// </example>
        /// <remarks>
        /// The property names on the find criteria object must to match a property name
        /// defined in the accompanying <see cref="HtmlControl.PropertyNames"/> object.
        /// </remarks>
        /// <seealso cref="UITestControlExtensions.Filter"/>
        public static UITestControl Find(this UITestControl container, object findCriteria)
        {
            return container.Find<UITestControl>(findCriteria);
        }

        /// <summary>
        /// Finds a control of the specified type.
        /// </summary>
        /// <typeparam name="T">The type of the control to find</typeparam>
        /// <param name="container">The container.</param>
        /// <returns>The first HtmlControl of the specified type</returns>
        /// <example>
        /// Find the (first) HtmlTable on the page
        /// <code>
        /// <![CDATA[
        /// HtmlTable onlyTable = browser.Find<HtmlTable>();
        /// ]]>
        /// </code>
        /// </example>
        public static T Find<T>(this UITestControl container) where T : UITestControl, new()
        {
            return Find<T>(container, new { });
        }

        /// <summary>
        /// Finds the control with the specified id.
        /// </summary>
        /// <typeparam name="T">The type of the control to find</typeparam>
        /// <param name="container">The container.</param>
        /// <param name="id">The id.</param>
        /// <returns>The found control</returns>
        /// <remarks>
        /// This method will search for controls which id contains the specified id.
        /// This is especially convenient when dealing with ASP.NET generated id's.
        /// </remarks>
        public static T Find<T>(this UITestControl container, string id) where T : UITestControl, new()
        {
            return Find<T>(container, new { Id = id }, PropertyExpressionOperator.Contains);
        }

        /// <summary>
        /// Finds the control with the specified id.
        /// </summary>
        /// <typeparam name="T">The type of the control to find</typeparam>
        /// <param name="container">The container.</param>
        /// <param name="id">The id.</param>
        /// <param name="expressionOperator">The expression operator.</param>
        /// <returns>The found control</returns>
        public static T Find<T>(this UITestControl container, string id, PropertyExpressionOperator expressionOperator) where T : UITestControl, new()
        {
            return Find<T>(container, new { Id = id }, expressionOperator);
        }

        /// <summary>
        /// Finds the control matching the specified criterias.
        /// </summary>
        /// <typeparam name="T">The type of the control to find</typeparam>
        /// <param name="container">The container.</param>
        /// <param name="findCriterias">The find criterias.</param>
        /// <returns>The found control</returns>
        /// <example>
        /// Find and click on the button with id 'MyButton'
        /// <code>
        /// <![CDATA[
        /// HtmlButton button = browser.Find<HtmlButton>(new { Id = "MyButton" });
        /// button.Click();
        /// ]]>
        /// </code>
        /// </example>
        public static T Find<T>(this UITestControl container, object findCriterias) where T : UITestControl, new()
        {
            return Find<T>(container, findCriterias, PropertyExpressionOperator.EqualTo);
        }

        /// <summary>
        /// Finds the control matching the specified criterias.
        /// </summary>
        /// <typeparam name="T">The type of the control to find</typeparam>
        /// <param name="container">The container.</param>
        /// <param name="findCriterias">The find criterias.</param>
        /// <param name="expressionOperator">The expression operator.</param>
        /// <returns>The found control</returns>
        /// <example>
        /// Find and click on the button which id contains 'MyButton'
        /// <code>
        /// <![CDATA[
        /// HtmlButton button = browser.Find<HtmlButton>(new { Id = "MyButton" }, PropertyExpressionOperator.Contains);
        /// button.Click();
        /// ]]>
        /// </code>
        /// </example>
        public static T Find<T>(this UITestControl container, object findCriterias, PropertyExpressionOperator expressionOperator) where T : UITestControl, new()
        {
            var element = new T { Container = container };
            findCriterias.ForEachProperty((prop, val) =>
                                          element.SearchProperties.Add(prop, val, expressionOperator));

            return element;
        }

        /// <summary>
        /// Finds the control with a <see cref="HtmlControl.PropertyNames.ControlDefinition"/> 
        /// containing the specified attribute with the specified value.
        /// </summary>
        /// <typeparam name="T">The type of the control to find</typeparam>
        /// <param name="container">The container.</param>
        /// <param name="attributeName">Name of the attribute.</param>
        /// <param name="attributeValue">The attribute value.</param>
        /// <returns>The found control</returns>
        /// <remarks>
        /// This method is primarily used for attributes that're not available in the PropertyNames collection.
        /// For instance "data dash" attributes (data-*).
        /// </remarks>
        /// <example>
        /// Find a control whith the 'data-vslmsg-for' attribute set to 'FieldName'
        /// <code>
        /// <![CDATA[
        /// HtmlSpan validationSpan = browser.Find<HtmlSpan>("data-valmsg-for", "FieldName");
        /// string validationMessage = validationSpan.InnerText;
        /// ]]>
        /// </code>
        /// </example>
        public static T Find<T>(this UITestControl container, string attributeName, string attributeValue) where T : UITestControl, new()
        {
            var element = new T { Container = container };
            element.SearchProperties.Add(
                HtmlControl.PropertyNames.ControlDefinition,
                string.Format("{0}=\"{1}\"", attributeName, attributeValue),
                PropertyExpressionOperator.Contains);
            return element;
        }

        /// <summary>
        /// Performs the provided acton if the specified control exists
        /// </summary>
        /// <typeparam name="T">The type of the control to find</typeparam>
        /// <param name="container">The container.</param>
        /// <param name="findCriterias">The find criterias.</param>
        /// <param name="doThis">The action to perform</param>
        /// <returns>True if a matching control was found. Otherwise false.</returns>
        /// <example>
        /// If the log off link is found - click on it
        /// <code>
        /// <![CDATA[
        /// browser.IfExists<HtmlHyperlink>(new { InnerText = "Log Off"}, e => e.Click());
        /// ]]>
        /// </code>
        /// </example>
        public static bool IfExists<T>(this UITestControl container, object findCriterias, Action<T> doThis) where T : HtmlControl, new()
        {
            var element = Find<T>(container, findCriterias);
            if (element.Exists)
            {
                doThis(element);
                return true;
            }

            return false;
        }

        #endregion find

        #region filter

        /// <summary>
        /// Returns the specified control instance
        /// </summary>
        /// <typeparam name="T">The type of the element</typeparam>
        /// <param name="element">The element.</param>
        /// <param name="instanceIndex">Index of the instance.</param>
        /// <returns>The control at the specified index in the result list</returns>
        /// <remarks>
        /// When the find method finds multiple matching controls this method 
        /// can be used to retrieve a single control from the collection
        /// </remarks>
        /// <example>
        /// Sets the selected item of the 2nd ComboBox on the page
        /// <code>
        /// <![CDATA[
        /// browser
        ///     .Find<HtmlComboBox>()
        ///     .Instance(2)
        ///     .SelectedItem = "SelectMe";
        /// ]]>
        /// </code>
        /// </example>
        public static T Instance<T>(this T element, int instanceIndex) where T : HtmlControl, new()
        {
            element.FilterProperties.Add(HtmlControl.PropertyNames.TagInstance, instanceIndex.ToString());
            return element;
        }

        /// <summary>
        /// Adds the specified filter properties to the element
        /// </summary>
        /// <typeparam name="T">The type of the element</typeparam>
        /// <param name="element">The element.</param>
        /// <param name="filterModel">The filter model.</param>
        /// <returns>The element matching the filter properties</returns>
        /// <remarks>Applied after search properties. Must be predecessed by Find.</remarks>
        /// <example>
        /// Since row and column index aren't part of the PropertyNames type for <see cref="HtmlCell"/>,
        /// Filter needs to be used in order to find a specific table cell.
        /// <code>
        /// <![CDATA[
        /// browser
        ///     .Find<HtmlCell>()
        ///     .Filter(new {
        ///         RowIndex    = "3"
        ///         ColumnIndex = "2"
        ///     });
        /// ]]>
        /// </code>
        /// </example>
        public static T Filter<T>(this T element, object filterModel) where T : HtmlControl, new()
        {
            filterModel.ForEachProperty((prop, val) =>
                element.FilterProperties.Add(prop, val));
            return element;
        }

        #endregion

        #region value helpers

        /// <summary>
        /// Gets the inner text of the control.
        /// </summary>
        /// <param name="self">The self.</param>
        /// <returns>The inner text of the control.</returns>
        /// <remarks>If the control isn't a <see cref="HtmlControl"/>, <see cref="string.Empty"/> will be returned</remarks>
        public static string GetInnerText(this UITestControl self)
        {
            if (!(self is HtmlControl))
            {
                return string.Empty;
            }

            return ((HtmlControl)self).InnerText;
        }

        /// <summary>
        /// Gets the inner text of a table cell
        /// </summary>
        /// <param name="table">The table</param>
        /// <param name="row">The row index</param>
        /// <param name="column">The column index</param>
        /// <returns>The inner text</returns>
        public static string GetValueInCell(this HtmlTable table, int row, int column)
        {
            return table
                .Find<HtmlCell>()
                .Filter(new
                {
                    RowIndex = row.ToString(),
                    ColumnIndex = column.ToString()
                })
               .InnerText
               .Trim();
        }

        #endregion
    }
}